package xorm

import (
	"fmt"
	"gitee.com/stevenchin/gin_study/xorm/initxorm"
	"github.com/gin-gonic/gin"
	"math/rand"
	"net/http"
	"time"
)

/*
插入数据。支持执行SQL与ORM两种方式插入数据
*/

// 一. ORM方式插入数据
// 插入数据使用Insert方法，Insert方法的参数可以是一个或多个Struct的指针，一个或多个Struct的Slice的指针。如果传入的是Slice
// 并且当数据库支持批量插入时，Insert会使用批量插入的方式进行插入

// 1. 插入一条数据，此时可以用Insert或者InsertOne。返回影响行数。如果需要获取新增后的id值，直接从结构体对象中获取。
// 	  因为在插入单条数据成功后，如果该结构体有自增字段(设置为autoincr)，则自增字段会被自动赋值为数据库中的id。这里需要注意的是，
// 	  如果插入的结构体中，自增字段已经赋值，则该字段会被作为非自增字段插入
func InsertOne(ctx *gin.Context) {

	user := new(user)
	if err := ctx.ShouldBindJSON(user); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
		return
	}
	// 也可使用orm1.Engine.InsertOne()方法
	if affected, err := initxorm.Engine.Insert(user); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
	} else {
		//给出新增后的id和插入行数
		ctx.JSON(http.StatusOK, gin.H{"id": user.Id, "affected": affected})
	}
}

// 2. 插入同一个表的多条数据，此时如果数据库支持批量插入那么会进行批量插入（不是多条插入语句分开执行，通过控制台打印可知），
// 	  但是这样每条记录就无法被自动赋予id值。如果数据库不支持批量插入，那么就会一条一条插入
func InsertBatchNotReturnId(ctx *gin.Context) {

	// 使用结构体作为属性，即嵌套结构体来接收json数组
	array := &userArray{}
	if err := ctx.ShouldBindJSON(array); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
		return
	}
	users := array.Users
	if len(users) == 0 {
		ctx.String(http.StatusInternalServerError, "需要插入数据为空")
		return
	}
	if i, err := initxorm.Engine.Insert(users); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
	} else {
		int64s := make([]int64, len(users))
		for i := 0; i < len(users); i++ {
			int64s[i] = users[i].Id
		}
		ctx.JSON(http.StatusOK, gin.H{"ids": int64s, "affected": i})
	}
}

// 3. 插入多条记录并且不使用批量插入，此时实际生成多条插入语句（通过控制台打印日志便可知道），每条记录均会自动赋予Id值
func InsertBatchReturnId(ctx *gin.Context) {

	// 使用结构体作为属性，即嵌套结构体来接收json数组
	array := &userArray{}
	if err := ctx.ShouldBindJSON(array); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
		return
	}
	users := array.Users
	if len(users) == 0 {
		ctx.String(http.StatusInternalServerError, "需要插入数据为空")
		return
	}

	// 因为...展开式只接受interface类型数据，所以这里使用slice转换一下。通过slice的基础知识可知，插入语句实际操作的还是
	// slice对应的数组，所以后面拿取插入id还是从user结构体数组中获取的
	sliceUsers := make([]interface{}, len(users))
	for key, value := range users {
		sliceUsers[key] = value
	}
	if i, err := initxorm.Engine.Insert(sliceUsers...); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
	} else {
		int64s := make([]int64, len(users))
		for i := 0; i < len(users); i++ {
			int64s[i] = users[i].Id
		}
		ctx.JSON(http.StatusOK, gin.H{"ids": int64s, "affected": i})
	}
}

// 4. 插入不同表的各一条记录（这里直接模拟数据了，不从context中获取）
func InsertBatchTable(ctx *gin.Context) {

	user := new(user)
	user.Name = fmt.Sprintf("steven at %d", time.Now().Second())
	user.Password = user.Name + "is password"
	basic := new(basic)
	basic.Name = user.Name
	basic.Password = user.Password
	basic.Age = 20
	if i, err := initxorm.Engine.Insert(user, basic); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
	} else {
		ctx.JSON(http.StatusOK, gin.H{"userId": user.Id, "basicId": basic.Id, "affected": i})
	}
}

// 5. 插入不同表的多条记录（这里直接模拟数据了，不从context中获取）。需要注意的点
// 	· 这里虽然支持同时插入，但这些插入并没有事务关系。因此有可能在中间插入出错后，后面的插入将不会继续。此时前面的插入已经成功，
//     如果需要回滚，请开启事物；
// 	· 批量插入会自动生成Insert into table values (),(),()的语句，因此各个数据库对SQL语句有长度限制，因此这样的语句有一个最大
// 	   的记录数，根据经验测算在150条左右。大于150条后，生成的sql语句将太长可能导致执行失败。因此在插入大量数据时，目前需要自
// 	   行分割成每150条插入一次
func InsertBatchTableAndData(ctx *gin.Context) {

	users := make([]*user, 10)
	basics := make([]*basic, 10)
	for i := range users {
		i2 := new(user)
		i2.Name = fmt.Sprintf("steven at %d", rand.Int31())
		i2.Password = i2.Name + "is password"
		users[i] = i2
	}
	for key := range basics {
		i := new(basic)
		i.Name = fmt.Sprintf("steven at %d", rand.Int31())
		i.Password = i.Name + "is password"
		i.Age = 20
		basics[key] = i
	}
	if i, err := initxorm.Engine.Insert(users, basics); err != nil {
		ctx.String(http.StatusInternalServerError, err.Error())
	} else {
		ctx.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "msg": "success", "data": i})
	}
}

// 二. 执行SQL命令插入数据
// TIPS：这里不做过多介绍，如有需要参考：https://www.kancloud.cn/xormplus/xorm/167095

// 因为使用orm1.Engine的Mapper为SameMapper，所以定义表结构体名称对应的表名相同
type user struct {

	// 如果field名称为Id而且类型为int64并且没有定义tag，则会被xorm视为主键，并且拥有自增属性。如果想用Id以外的名字或
	// 非int64类型做为主键名， 必须在对应的Tag上加上xorm:"pk"来定义主键，加上xorm:"autoincr"作为自增
	Id       int64
	Name     string
	Password string
	// - : 这个Field将不进行字段映射
	// -> : 这个Field将只写入到数据库而不从数据库读取
	// <- : 这个Field将只从数据库读取，而不写入到数据库
	// 对应mysql表字段名为is_lock，需要指定
	IsLock int8 `xorm:" -> 'is_lock'"`
}

// 批量插入接收json数据结构体
// 模拟数据为：
//	{
//		"users":[
//			{
//			"name": "ro213233322",
//			"password":"admin"
//
//			},
//			{
//			"name": "root213232",
//			"password":"admin"
//
//			}
//		]
//	}
type userArray struct {
	Users []*user
}

// basic 表对应的结构体
type basic struct {
	Id       int64
	Name     string `xorm:"notnull"`
	Password string `xorm:"notnull"`
	Age      int8   `xorm:"notnull"`
	// Created可以让您在数据插入到数据库时自动将对应的字段设置为当前时间，需要在xorm标记中使用created标记，如下所示进行标记，
	// 对应的字段可以为time.Time或者自定义的time.Time或者int,int64等int类型。在Insert()或InsertOne()方法被调用时，created标记
	// 的字段将会被自动更新为当前时间或者当前时间的秒数（对应为time.Unix())
	// TIPS：默认xorm采用Local时区，所以默认调用的time.Now()会先被转换成对应的时区。要改变xorm的时区，可以使用：
	// orm1.Engine.TZLocation, _ = time.LoadLocation("Asia/Shanghai")
	CreateTime int64 `xorm:"created 'create_time'"`
}
